/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.rest.meta.service;

import static java.util.Objects.isNull;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.context.ModelChangeContext.ModelChangeType;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.context.UpsertEnumerationsContext;
import org.unidata.mdm.meta.type.instance.EnumerationsInstance;
import org.unidata.mdm.meta.type.model.enumeration.EnumerationsModel;
import org.unidata.mdm.rest.meta.converter.EnumerationConverter;
import org.unidata.mdm.rest.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.rest.meta.ro.EnumerationDefinitionRO;
import org.unidata.mdm.rest.system.ro.ErrorResponse;
import org.unidata.mdm.rest.system.ro.RestResponse;
import org.unidata.mdm.rest.system.ro.UpdateResponse;
import org.unidata.mdm.rest.system.service.AbstractRestService;
import org.unidata.mdm.system.exception.PlatformBusinessException;
import org.unidata.mdm.system.serialization.xml.XmlObjectSerializer;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * @author Michael Yashin. Created on 19.05.2015.
 */
@Path(EnumerationRestService.SERVICE_PATH)
@Consumes({ "application/json" })
@Produces({ "application/json" })
public class EnumerationRestService extends AbstractRestService {

    public static final String SERVICE_PATH = "enumerations";

    @Autowired
    private MetaModelService metaModelService;
    /**
     * Gets all enumerations.
     *
     * @return response
     */
    @GET
    @Operation(
        description = "Enumerations list.",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = List.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response findAll() {

        List<EnumerationDefinitionRO> target = metaModelService.instance(Descriptors.ENUMERATIONS)
            .getEnumerations().stream()
                .map(EnumerationConverter::to)
                .collect(Collectors.toList());

        return ok(target);
    }
    /**
     * Creates new enumeration.
     *
     * @param req enumeration definition.
     * @return HTTP response.
     */
    @POST
    @Operation(
        description = "Creates an enumeration.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = EnumerationDefinitionRO.class)), description = "Insert request."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = UpdateResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response create(final EnumerationDefinitionRO req) {

        boolean exists = metaModelService.instance(Descriptors.ENUMERATIONS)
            .getEnumeration(req.getName()) != null;

        if (exists) {
            throw new PlatformBusinessException(
                    "Enumeration with this name already exists!",
                    MetaRestExceptionIds.EX_META_ENUMERATION_ALREADY_EXISTS);
        }

        metaModelService.upsert(
            UpsertEnumerationsContext.builder()
                .update(EnumerationConverter.from(req))
                .build());

        return ok(new UpdateResponse(true, req.getName()));
    }
    /**
     * Delete existing enumeration.
     *
     * @param enumerationName the enumeration name
     * @return HTTP response.
     */
    @DELETE
    @Path("{enumerationName}")
    @Operation(
        description = "Removes an existing source system.",
        method = HttpMethod.DELETE,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = String.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response delete(@Parameter(description = "Enumeration id.", in = ParameterIn.PATH) @PathParam(value = "enumerationName") String enumerationName) {

        boolean absent = metaModelService.instance(Descriptors.ENUMERATIONS)
            .getEnumeration(enumerationName) == null;

        if (absent) {
            throw new PlatformBusinessException(
                    "Enumeration with this name not found!",
                    MetaRestExceptionIds.EX_META_ENUMERATION_NOT_FOUND);
        }

        metaModelService.upsert(
            UpsertEnumerationsContext.builder()
                .delete(enumerationName)
                .build());

        return ok(new RestResponse<>(null));
    }
    /**
     * Updates existing enumeration.
     *
     * @param enumerationName the enumeration name
     * @param req              Updated enumeration. {@see EnumerationDefinitionRO}
     * @return HTTP response.
     */
    @PUT
    @Path("{enumerationName}")
    @Operation(
        description = "Update or rename an existing enumeration. If enumeration's name in the path and the name in the body differ, the request is interpreted as renaming action.",
        method = HttpMethod.PUT,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = EnumerationDefinitionRO.class)), description = "Update request"),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = UpdateResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response update(
            @Parameter(description = "Enumeration name.", in = ParameterIn.PATH) @PathParam("enumerationName") String enumerationName,
            EnumerationDefinitionRO req) {

        metaModelService.upsert(
            UpsertEnumerationsContext.builder()
                .update(EnumerationConverter.from(req))
                .delete(!StringUtils.equals(enumerationName, req.getName()) ? enumerationName : null)
                .build());

        // needed for sencha
        return ok(new UpdateResponse(true, req.getName()));
    }
    /**
     * Gets existing enumeration by name.
     *
     * @param enumerationName the enumeration name
     * @return enumeration definition. {@see EnumerationDefinitionRO}
     */
    @GET
    @Path("{enumerationName}")
    @Operation(
        description = "Get an existing source system by name",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = EnumerationDefinitionRO.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response get(@Parameter(description = "Enumeration name.", in = ParameterIn.PATH) @PathParam("enumerationName") String enumerationName) {
        return ok(EnumerationConverter.to(metaModelService
                .instance(Descriptors.ENUMERATIONS)
                .getEnumeration(enumerationName)));
    }
    @POST
    @Path("/import")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Operation(
        description = "Import full enumerations model. Only xml is supported.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA), description = "Source request."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = UpdateResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response source(@Multipart(required = true, value = "file") Attachment attachment) throws IOException {

        if (isNull(attachment)) {
            return okOrNotFound(null);
        }

        if (!MediaType.TEXT_XML_TYPE.equals(attachment.getContentType())) {
            throw new PlatformBusinessException("Import of enumerations failed. Invalid media type [{}]. XML is expected.",
                    MetaRestExceptionIds.EX_META_ENUMERATION_IMPORT_UNSUPPORTED, attachment.getContentType().toString());
        }

        EnumerationsModel values = XmlObjectSerializer.getInstance()
                .fromXmlInputStream(EnumerationsModel.class, attachment.getObject(InputStream.class));

        if (CollectionUtils.isEmpty(values.getValues())) {
            throw new PlatformBusinessException("Import of enumerations failed. Empty definition.",
                    MetaRestExceptionIds.EX_META_ENUMERATION_IMPORT_EMPTY);
        }

        metaModelService.upsert(UpsertEnumerationsContext.builder()
            .upsertType(ModelChangeType.FULL)
            .update(values.getValues())
            .storageId(values.getStorageId())
            .name(values.getName())
            .build());

        return ok(new RestResponse<>());
    }

    @GET
    @Path("/export")
    @Produces(MediaType.TEXT_XML)
    @Operation(
        description = "Dump measurement units model.",
        method = HttpMethod.GET,
        parameters = { @Parameter(description = "Storage ID. Optional", in = ParameterIn.QUERY, name = "storageId") },
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = StreamingOutput.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response dump(@QueryParam("storageId") String storageId) throws UnsupportedEncodingException {

        EnumerationsInstance current = StringUtils.isBlank(storageId)
                ? metaModelService.instance(Descriptors.ENUMERATIONS)
                : metaModelService.instance(Descriptors.ENUMERATIONS, storageId, null);

        if (Objects.isNull(current) || current.isEmpty()) {
            return okOrNotFound(null);
        }

        EnumerationsModel source = current.toSource();
        final String encodedFilename = URLEncoder.encode(
                "enumerations-" + DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd_HH-mm-ss") + ".xml",
                StandardCharsets.UTF_8.name());

        StreamingOutput result = output -> {
            try {
                output.write(
                    XmlObjectSerializer.getInstance()
                        .toXmlString(source, true)
                        .getBytes(StandardCharsets.UTF_8));
            } catch (Exception e) {
                throw new PlatformBusinessException("Enumerations marshaling failed.",
                        MetaRestExceptionIds.EX_META_ENUMERATION_MARSHALING_FAILED);
            }
        };

        return Response.ok(result)
                .encoding(StandardCharsets.UTF_8.name())
                .header("Content-Disposition", "attachment; filename=" + encodedFilename + "; filename*=UTF-8''" + encodedFilename)
                .header("Content-Type", MediaType.TEXT_XML)
                .build();
    }
}
